package org.afs.gescol.edt.api.implementations;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.afs.gescol.core.dto.classes.FullSchoolClassDto;
import org.afs.gescol.core.dto.subject.FullSubjectDto;
import org.afs.gescol.core.dto.time.HourlyDto;
import org.afs.gescol.core.dto.time.PeriodDto;
import org.afs.gescol.core.dto.time.SchoolHoursDto;
import org.afs.gescol.core.service.interfaces.GlobalParamsServices;
import org.afs.gescol.core.service.interfaces.ReferenceServices;
import org.afs.gescol.edt.api.TimesTableService;
import org.afs.gescol.edt.api.dao.interfaces.TimeSlotDao;
import org.afs.gescol.edt.api.dto.TimeSlotCriteria;
import org.afs.gescol.edt.api.dto.TimeSlotDto;
import org.afs.gescol.edt.api.generator.InputData;
import org.afs.gescol.edt.api.generator.OutputData;
import org.afs.gescol.edt.api.generator.TimesTableGenerator;
import org.afs.gescol.edt.api.model.TimeSlot;
import org.afs.gescol.edt.api.util.EdtUtilities;
import org.afs.gescol.edt.api.util.MessageKeyUtil;
import org.scub.foundation.framework.base.exception.BusinessException;
import org.scub.foundation.framework.base.exception.TechnicalException;
import org.scub.foundation.framework.base.mapping.util.MapperDozerBean;
import org.scub.foundation.framework.base.messagesource.MessageSourceUtil;

/**
 * Implementation of timestable services.
 * @author Kobla GBENYO (kobla.gbenyo@gmail.com)
 */
public class TimesTableServiceImpl implements TimesTableService {

    private MessageSourceUtil messageSourceUtil;

    private MapperDozerBean mapperDozerBean;

    private ReferenceServices referenceServices;

    private GlobalParamsServices globalParamsServices;

    private TimesTableGenerator timesTableGenerator;

    private TimeSlotDao timeSlotDao;

    private Map<Long, List<List<Integer>>> teacherAvail = new HashMap<Long, List<List<Integer>>>();

    private Map<Long, List<List<Integer>>> classAvail = new HashMap<Long, List<List<Integer>>>();

    private PeriodDto period;

    private List<HourlyDto> hourly;

    @Override
    public TimeSlotDto getTimeSlotById(Long id) {
        if (id == null) {
            MessageKeyUtil.getInstance();
            throw new TechnicalException(messageSourceUtil.get(MessageKeyUtil.ERROR_NULL_TIME_SLOT_ID));
        }
        final TimeSlot timeSlot = timeSlotDao.getTimeSlotById(id);
        if (timeSlot == null) {
            throw new TechnicalException(messageSourceUtil.get(MessageKeyUtil.ERROR_TIME_SLOT_NON_EXISTENT, new String[]{id.toString()}));
        }
        // map DB data to DTO
        final TimeSlotDto timeSlotDto = mapperDozerBean.map(timeSlot, TimeSlotDto.class);
        setAdditionalDataToTimeSlot(timeSlot, timeSlotDto);
        return timeSlotDto;
    }

    @Override
    public List<TimeSlotDto> getTimeSlotByCriteria(TimeSlotCriteria criteria) {
        if (criteria == null) {
            throw new TechnicalException(messageSourceUtil.get(MessageKeyUtil.ERROR_NULL_TIME_SLOT_CRITERIA));
        }
        // specify one of this criteria (begin date, end date, subject, entity, leader) to perform well the search.
        if (criteria.getBeginDate() == null && criteria.getEndDate() == null && criteria.getSubject() == null
                && criteria.getEntity() == null && criteria.getLeader() == null) {
            throw new BusinessException(messageSourceUtil.get(MessageKeyUtil.ERROR_TIME_SLOT_MORE_CRITERIA));
        }
        final List<TimeSlot> timeSlots = timeSlotDao.getTimeSlotByCriteria(criteria);
        // map DB data to DTO
        final List<TimeSlotDto> timeSlotList = new ArrayList<TimeSlotDto>();
        if (timeSlots != null) {
            for (TimeSlot timeSlot : timeSlots) {
                final TimeSlotDto timeSlotDto = mapperDozerBean.map(timeSlot, TimeSlotDto.class);
                setAdditionalDataToTimeSlot(timeSlot, timeSlotDto);
                timeSlotList.add(timeSlotDto);
            }
        }
        return timeSlotList;
    }

    @Override
    public void generateTimesTable(Long cycleId) {
        period = globalParamsServices.getCurrentperiod(cycleId);
        if (period == null) {
            throw new BusinessException(messageSourceUtil.get(MessageKeyUtil.ERROR_TIME_TABLE_NO_CURRENT_PERIOD, new String[]{cycleId.toString()}));
        }
        hourly = globalParamsServices.getHourly(cycleId);
        if (hourly == null || hourly.isEmpty()) {
            throw new BusinessException(messageSourceUtil.get(MessageKeyUtil.ERROR_TIME_TABLE_NO_HOURLY, new String[]{cycleId.toString()}));
        }
        timeSlotDao.cleanTimeSlot();
        final SchoolHoursDto ref = globalParamsServices.getSchoolHoursParams(cycleId);
        final List<FullSchoolClassDto> classes = globalParamsServices.getSchoolClassFullData(cycleId);
        if (ref != null && classes != null) {
            for (FullSchoolClassDto schoolClass : classes) {
                for (FullSubjectDto subject : schoolClass.getSubjects()) {
                    final int totalTeachers = subject.getTeachers().size();
                    int size = 0;
                    for (Long teacherId : subject.getTeachers()) {
                        size++;
                        // construct the input for gerenating the programs
                        final InputData input = new InputData(ref.getDayNumber(), ref.getDailyTimeSlotMax(), ref.getDailyTimeTableMax(),
                            subject.getWeeklyHoursVolume(), getTeacherAvailability(ref, teacherId), getClassAvailability(ref, schoolClass.getId()));
                        // generate programs
                        final OutputData output = timesTableGenerator.computeTimestable(input);
                        // if all hours is are programmed, we save the programs
                        if (output.getRemainingHours() == 0) {
                            savePrograms(ref, schoolClass.getId(), subject.getId(), teacherId, output);
                            break;
                        } else if (totalTeachers == size) {
                            // if not if we are at the end of teachers list, we save the incomplete programs
                            savePrograms(ref, schoolClass.getId(), subject.getId(), teacherId, output);
                        }
                    }
                }
            }
        }
    }

    private void setAdditionalDataToTimeSlot(TimeSlot timeSlot, TimeSlotDto timeSlotDto) {
        if (timeSlot.getSubjectEid() != null && EdtUtilities.isNumeric(timeSlot.getSubjectEid())) {
            timeSlotDto.setSubject(referenceServices.getSubjectById(Long.valueOf(timeSlot.getSubjectEid())));
        }
        if (timeSlot.getEntityEid() != null && EdtUtilities.isNumeric(timeSlot.getEntityEid())) {
            timeSlotDto.setEntity(referenceServices.getSchoolClassById(Long.valueOf(timeSlot.getEntityEid())));
        }
        if (timeSlot.getLeaderEid() != null && EdtUtilities.isNumeric(timeSlot.getLeaderEid())) {
            timeSlotDto.setLeader(referenceServices.getTeacherById(Long.valueOf(timeSlot.getLeaderEid())));
        }
    }

    private List<List<Integer>> getClassAvailability(SchoolHoursDto hours, Long schoolClassId) {
        if (!classAvail.containsKey(schoolClassId)) {
            updateAvailabilities(classAvail, schoolClassId, hours.getDayNumber(), hours.getDailyTimeTableMax(), null);
        }
        return classAvail.get(schoolClassId);
    }

    private List<List<Integer>> getTeacherAvailability(SchoolHoursDto hours, Long teacherId) {
        if (!teacherAvail.containsKey(teacherId)) {
            updateAvailabilities(teacherAvail, teacherId, hours.getDayNumber(), hours.getDailyTimeTableMax(), null);
        }
        return teacherAvail.get(teacherId);
    }

    private void savePrograms(SchoolHoursDto hours, Long schoolClassId, Long subjectId, Long teacherId, OutputData output) {
        final Calendar periodBeginDate = getCalendar((Date) period.getBeginDate().clone());
        final Calendar periodEndDate = getCalendar(period.getEndDate());
        // bring begin date day to the first day of the week : MONDAY
        periodBeginDate.add(Calendar.DAY_OF_MONTH, Calendar.MONDAY - periodBeginDate.get(Calendar.DAY_OF_WEEK));
        // for each day in the current period
        while (periodBeginDate.before(periodEndDate)) {
            for (List<Integer> programmedHours : output.getProgrammedHours()) {
                // get the timeslot dates corresponding to the programs
                final List<TimeSlotDates> dates = getDatesByPrograms(periodBeginDate.getTime(), programmedHours);
                // save timeslot corresponding to the programs
                for (TimeSlotDates timeslotDate : dates) {
                    final TimeSlot timeslot = new TimeSlot();
                    timeslot.setBeginDate(getCalendar(timeslotDate.getBeginDate()));
                    timeslot.setEndDate(getCalendar(timeslotDate.getEndDate()));
                    timeslot.setSubjectEid(subjectId.toString());
                    timeslot.setEntityEid(schoolClassId.toString());
                    timeslot.setLeaderEid(teacherId.toString());
                    timeSlotDao.createTimeSlot(timeslot);
                }
                // go to the next day
                periodBeginDate.roll(Calendar.DATE, true);
            }
            // exclude the week-end
            periodBeginDate.add(Calendar.DAY_OF_MONTH, 7 - hours.getDayNumber());
        }
        // remove the saved programs from the availabilities
        updateAvailabilities(teacherAvail, teacherId, hours.getDayNumber(), hours.getDailyTimeTableMax(), output.getProgrammedHours());
        updateAvailabilities(classAvail, schoolClassId, hours.getDayNumber(), hours.getDailyTimeTableMax(), output.getProgrammedHours());
    }

    private List<TimeSlotDates> getDatesByPrograms(Date day, List<Integer> programmedHours) {
        final List<TimeSlotDates> dates = new ArrayList<TimeSlotDates>();
        // MODE : hourly divided in one hour (activate it if necessary)
        //for (Integer program : programmedHours) {
        //    dates.add(getHourly(day, program, program));
        //}

        // MODE : hourly grouped it they are followed
        final List<Integer[]> grouped = EdtUtilities.groupIntegerList(programmedHours);
        for (Integer[] program : grouped) {
            dates.add(getHourly(day, program[0], program[1]));
        }
        return dates;
    }

    private void updateAvailabilities(Map<Long, List<List<Integer>>> repo, Long key, int dayNumber, int dailyTimeTableMax, List<List<Integer>> programs) {
        final List<List<Integer>> availabilities = new ArrayList<List<Integer>>();
        for (int j = 0; j < dayNumber; j++) { // for each day
            final List<Integer> avail = new ArrayList<Integer>();
            for (int i = 0; i < dailyTimeTableMax; i++) { // for each hours in the day
                final Integer hour = new Integer(i + 1);
                if (programs == null || !programs.get(j).contains(hour)) {
                    avail.add(hour);
                }
            }
            availabilities.add(avail);
        }
        repo.put(key, availabilities);
    }

    private Calendar getCalendar(Date date) {
        final Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        return calendar;
    }

    private TimeSlotDates getHourly(Date day, int beginHourly, int endHourly) {
        Date begin = null, end = null;
        for (HourlyDto hour : hourly) {
            if (hour.getOrder() == beginHourly) {
                begin = hour.getBeginHour();
            }
            if (hour.getOrder() == endHourly) {
                end = hour.getEndHour();
            }
            if (begin != null && end != null) {
                break;
            }
        }
        // FIXME ensure that begin & end are not null
        final TimeSlotDates date = new TimeSlotDates(new Date(day.getYear(), day.getMonth(), day.getDate(), begin.getHours(), begin.getMinutes(), 0),
            new Date(day.getYear(), day.getMonth(), day.getDate(), end.getHours(), end.getMinutes(), 0));
        return date;
    }

    private class TimeSlotDates {

        private Date beginDate;

        private Date endDate;

        public TimeSlotDates(Date beginDate, Date endDate) {
            this.beginDate = beginDate;
            this.endDate = endDate;
        }

        /**
         * Get the value of beginDate.
         * @return the beginDate
         */
        public Date getBeginDate() {
            return beginDate;
        }

        /**
         * Get the value of endDate.
         * @return the endDate
         */
        public Date getEndDate() {
            return endDate;
        }
    }

    /**
     * Set the value of messageSourceUtil.
     * @param messageSourceUtil the messageSourceUtil to set
     */
    public void setMessageSourceUtil(MessageSourceUtil messageSourceUtil) {
        this.messageSourceUtil = messageSourceUtil;
    }

    /**
     * Set the value of mapperDozerBean.
     * @param mapperDozerBean the mapperDozerBean to set
     */
    public void setMapperDozerBean(MapperDozerBean mapperDozerBean) {
        this.mapperDozerBean = mapperDozerBean;
    }

    /**
     * Set the value of timeSlotDao.
     * @param timeSlotDao the timeSlotDao to set
     */
    public void setTimeSlotDao(TimeSlotDao timeSlotDao) {
        this.timeSlotDao = timeSlotDao;
    }

    /**
     * Set the value of referenceServices.
     * @param referenceServices the referenceServices to set
     */
    public void setReferenceServices(ReferenceServices referenceServices) {
        this.referenceServices = referenceServices;
    }

    /**
     * Set the value of globalParamsServices.
     * @param globalParamsServices the globalParamsServices to set
     */
    public void setGlobalParamsServices(GlobalParamsServices globalParamsServices) {
        this.globalParamsServices = globalParamsServices;
    }

    /**
     * Set the value of timesTableGenerator.
     * @param timesTableGenerator the timesTableGenerator to set
     */
    public void setTimesTableGenerator(TimesTableGenerator timesTableGenerator) {
        this.timesTableGenerator = timesTableGenerator;
    }
}
